# !/usr/bin/env python
# -*- coding: utf-8 -*-
# ///////////////////////////////////////////////////////////////
# @File    :   online_upgrade.py
# @Time    :   2025/01/03 11:27:20
# @Author  :   kai.luo
# @Version :   1.0.0
# @Desc    :   None
# ///////////////////////////////////////////////////////////////

from PySide6.QtWidgets import *
from PySide6.QtGui import *
from PySide6.QtCore import *
import sys
import shutil
import os
import subprocess
# 样式
from ui.keil_Iar_ui import Ui_MainWindow


class UpdateProcess(QWidget, Ui_UpdateProcessForm):
    def __init__(self):
        super().__init__()

        # 初始化窗口
        self.setupUi(self)
        self.setWindowIcon(QIcon('ui/img/logo.ico'))
        self.setWindowTitle('Software Upgrading V1.0.0.1')
        self.move(100, 100)

        # 初始化参数
        self.local_xml_data = dict()
        self.remote_xml_data = dict()
        self.user_documents_path = os.path.expanduser('~\Documents\HK_Embedded_Specs')
        self.updater_config_file_name = 'Updater.xml'
        self.break_update_xml = False  # 默认已连接
        self.temp_add_files_list = list()  # 临时增加文件的列表
        self.temp_replace_files_list = list()  # 临时替换文件的列表

        self.request_num = 0  # 请求的次数

        # 显示界面后，单次触发检测
        self.timer = QTimer(self)
        self.timer.setSingleShot(True)
        self.timer.timeout.connect(self.initUpdate)

    def showEvent(self, event):
        super().showEvent(event)
        # 在页面显示完后启动定时器
        self.timer.start(1000)  # 1秒后触发定时

    def initUpdate(self):
        """
        点击确认，开始更新
        """

        # 获取本地更新文件数据
        self.printLog('消息', '开始解析本地 Updater.xml 文件.')
        self.parseLocalXml()

        # 获取服务器上更新文件数据
        self.printLog('消息', '开始获取服务器上 Updater.xml 文件.')
        self.parseRemoteXml()

        # 比较本地与服务器上xml文件的版本差别以及软件版本的差别
        self.compared_version()

    def parseLocalXml(self):
        """
        解析本地配置文件
        """
        # 判断本地是否存在 Updater.xml 文件
        updateXml_path = os.path.join(os.getcwd(), self.updater_config_file_name)
        if not os.path.isfile(updateXml_path):
            QMessageBox.information(self, "提示", '本地未找到更新文件 Updater.xml 文件.')
            os._exit(0)

        domTree = parse(updateXml_path)
        # 解析本都 Updater.xml 文件中目标数据
        self.parseXml('local', domTree)

        # 打印完成信息
        self.printLog('成功', '解析本地 Updater.xml 文件成功.')

    def parseRemoteXml(self):
        """
        解析服务器配置文件
        """
        # 服务器上 Updater.xml 的路径
        remote_project_url = self.local_xml_data.get('remoteProjectUrl', '')
        remote_xml_url = remote_project_url+'/Updater.xml'

        res = requests.get(remote_xml_url, stream=True)
        remote_config_file_str = res.content.decode('utf-8')

        if 'remoteProjectUrl' not in remote_config_file_str:
            if self.request_num > 5:
                QMessageBox.information(self, "提示", '从服务器上拉取 Updater.xml 文件失败')
                os._exit(0)
            else:
                self.printLog('消息', f'请耐心等待...')
                time.sleep(3)
                self.request_num += 1
                self.parseRemoteXml()
        else:
            # 解析服务器上 Updater.xml 文件中目标数据
            remote_domTree = parseString(remote_config_file_str)
            self.parseXml('remote', remote_domTree)

            # 打印完成信息
            self.printLog('成功', '解析服务器上 Updater.xml 文件成功.')

    def parseXml(self, xml_pos, domTree):
        """
        解析 xml文件
        """

        # 初始化参数
        files_version = dict()

        # 获得根节点
        rootNode = domTree.documentElement

        # 解析xml
        try:
            version = rootNode.getElementsByTagName('version')[0].firstChild.data  # 软件名称
            setupName = rootNode.getElementsByTagName('setupName')[0].firstChild.data  # 软件名称
            remoteProjectUrl = rootNode.getElementsByTagName('remoteProjectUrl')[0].firstChild.data  # 远程路径
            execFileName = rootNode.getElementsByTagName('execFileName')[0].firstChild.data  # 主程序文件名
        except:
            if xml_pos == 'local':
                self.printLog('错误', '解析本地 Updater.xml 中的节点失败.')
            else:
                self.printLog('错误', '解析服务器上 Updater.xml 中的节点失败.')

        # 获取更新的条目
        try:
            messages_list = []
            if rootNode.getElementsByTagName('messages'):
                for messages_node in rootNode.getElementsByTagName('messages'):
                    for node in messages_node.childNodes:
                        if node.nodeName == 'message':
                            messages_list.append(node.firstChild.data)
        except:
            if xml_pos == 'local':
                self.printLog('错误', '解析本地 Updater.xml中 messages 标签失败.')
            else:
                self.printLog('错误', '解析服务器上 Updater.xml中 messages 标签失败.')

        # 获取资源文件的路径与版本
        try:
            if rootNode.getElementsByTagName('file'):
                for file_node in rootNode.getElementsByTagName('file'):
                    temp_file = ''
                    temp_ver = ''
                    for node in file_node.childNodes:
                        if node.nodeName == 'name':   # ELEMENT_NODE
                            temp_file = node.firstChild.data
                        if node.nodeName == 'ver':
                            temp_ver = node.firstChild.data
                    files_version.setdefault(temp_file, temp_ver)
        except:
            if xml_pos == 'local':
                self.printLog('错误', '解析本地 Updater.xml中 files 标签失败.')
            else:
                self.printLog('错误', '解析服务器上 Updater.xml中 files 标签失败.')

        # 获取所以资源文件路径
        if xml_pos == 'local':
            self.local_xml_data = {'version': version,
                                   'setupName': setupName,
                                   'remoteProjectUrl': remoteProjectUrl,
                                   'files_version': files_version,
                                   'messages': messages_list}
        else:
            self.remote_xml_data = {'version': version,
                                    'setupName': setupName,
                                    'remoteProjectUrl': remoteProjectUrl,
                                    'execFileName': execFileName,
                                    'files_version': files_version,
                                    'messages': messages_list}

    def compared_version(self):
        """
        比较xml文件
        """
        self.add_files = dict()
        self.del_files = dict()
        self.replace_files = dict()

        local_version = self.local_xml_data.get('version', '')
        remote_version = self.remote_xml_data.get('version', '')

        # 获取本地版本
        if not local_version:
            self.printLog('错误', '本地 Updater.xml 中标签未发现版本号.')
            return

        # 获取服务器上版本
        if not remote_version:
            self.printLog('错误', '服务器上 Updater.xml 中标签未发现版本号.')
            return

        # 已确定需要升级
        local_version_num = int(''.join(local_version.split('.')))
        remote_version_num = int(''.join(remote_version.split('.')))

        # 对比version版本号
        self.printLog('消息', '开始对比服务器与本地软件版本的差异.')
        if local_version_num < remote_version_num:
            # 更新版本
            self.updateVersion()
        else:
            self.printLog('成功', '软件版本不需要更新.')
            #
            self.printLog('消息', '开始对比服务器与本地资源文件的差异.')
            # 获取需要更新的资源文件
            self.getFilesDiff()
            if not self.add_files and not self.del_files and not self.replace_files:
                self.printLog('成功', '资源文件不需要更新.')
                self.progressBar.setValue(100)
                return

            #  有资源需要更新
            self.updateSourceFiles()

            # 删除临时文件
            self.delTempFiles()

    def updateVersion(self):
        """
        升级软件
        remoteProjectUrl: http://ae-git.hsxp-cd.com:83/upload/HKPinMap
        remote_source_dir: http://ae-git.hsxp-cd.com:83/upload/1.0.1.4/source
        """

        # 执行文件名
        execFileName = self.remote_xml_data.get('execFileName')

        # 服务器上工程目录
        remote_project_url = self.remote_xml_data.get('remoteProjectUrl')

        # 服务器上版本号
        remote_version = self.remote_xml_data.get('version')

        # 服务器上软件包名称
        remote_setupName = self.remote_xml_data.get('setupName')

        # 服务器上软件包存放的路径
        remote_setup_url = remote_project_url+'/'+remote_version+'/app/'+remote_setupName

        # 用户目录下的工程目录不存,即新建
        os.makedirs(self.user_documents_path, exist_ok=True)

        self.printLog('消息', f'开始下载软件包 {remote_setupName}.')
        try:
            response = requests.get(remote_setup_url, stream=True)
        except:
            self.printLog('错误', f'连接服务器下载软件包 {remote_setupName} 失败.')
            return

        local_app_url = os.path.join(self.user_documents_path, remote_setupName)
        if response.status_code == 200:  # 判断是否响应成功
            size = 0  # 初始化已下载大小
            chunk_size = 1024  # 每次下载的数据大小

            try:
                if response.headers.get('content-length', ''):
                    content_size = int(response.headers['content-length'])  # 下载文件总大小
                else:
                    content_size = int(os.stat(response.content))  # 下载文件总大小
            except:
                self.printLog('错误', f'服务器上该路径 {local_app_url} 不存在.')
                return

            # 检查服务器是否中断
            for i in range(3):
                break_flag = self.checkBreakConnet()
                if break_flag:
                    self.printLog('告警', '正在尝试连接服务器,请耐心等待...')
                    time.sleep(3)
                    self.break_update_xml = True
                else:
                    break

            if self.break_update_xml:
                self.printLog('错误', '连接异常,请稍后重试...')
                return

            # 开始下载软件更新包
            try:
                with open(r''+local_app_url, 'wb') as file:  # 显示进度条
                    for data in response.iter_content(chunk_size=chunk_size):
                        if data:
                            file.write(data)
                            size += len(data)
                            rate = float(size / content_size * 100)
                            self.progressBar.setValue(int(rate))
                if self.progressBar.value() == 100:
                    self.printLog('成功', f'下载软件包 {remote_setupName} 成功.')

                    # 同步更新本地的配置文件
                    self.update_local_xml_file()
                    time.sleep(1)
                    self.close()
                    subprocess.Popen(local_app_url, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
                    # 重启主程序
                    # QProcess().startDetached(os.path.join(os.getcwd(), execFileName))
            except:
                self.printLog('错误', f'下载服务器上安装包 {remote_setupName} 到本地失败.')
        else:
            print(traceback.print_exc())
            self.printLog('错误', f'下载软件包 {remote_setupName} 失败.')

    def getFilesDiff(self):
        # 初始化参数
        # temp_del_files = {}
        temp_add_files = {}
        temp_replace_files = {}

        local_files_version = self.local_xml_data.get('files_version')
        remote_files_version = self.remote_xml_data.get('files_version')

        # 无资源的情况
        if not local_files_version and not remote_files_version:
            return

        if local_files_version and not remote_files_version:
            self.printLog('错误', '服务器上 Updater.xml 中files标签未发现子标签.')
            return

        # 服务器上临时增加资源文件的情况
        if not local_files_version and remote_files_version:
            self.add_files = remote_files_version  # 全量新增资源文件
        else:
            # 拷贝服务器上的配置文件的资源文件部分的标签值
            copy_local_files_version = copy.deepcopy(local_files_version)

            # 以服务器上的版本
            for remote_f, remote_v in remote_files_version.items():
                if local_files_version.get(remote_f, ''):
                    # 更新替换文件: 服务器上与本地都有该文件,但服务器上的版本大于本地版本
                    if remote_v > local_files_version[remote_f]:
                        temp_replace_files[remote_f] = remote_files_version[remote_f]

                    # 删除文件: 服务器上已经不存在该文件,但是本地还有
                    copy_local_files_version.pop(remote_f)
                else:
                    # 新增文件: 删除服务器上与本地相同的文件剩下的文件
                    temp_add_files[remote_f] = remote_v

            # 需要增加的文件
            self.add_files = temp_add_files

            # 需要删除的文件
            self.del_files = copy_local_files_version

            # 需要更新替换的文件
            self.replace_files = temp_replace_files

    def updateSourceFiles(self):
        """
        升级软件
        update_label:1更新文件数据，2更新软件
        remote_root_url: http://ae-git.hsxp-cd.com:83/upload
        remote_source_dir: http://ae-git.hsxp-cd.com:83/upload/1.0.1.4/source
        os.getcwd()-->D:\pyside2_hk_pin_map_v2\project
        """
        remoteProjectUrl = self.remote_xml_data.get('remoteProjectUrl')
        remote_version = self.remote_xml_data.get('version')
        remote_source_dir = remoteProjectUrl+'/'+remote_version+'/' + 'source'
        execFileName = self.remote_xml_data.get('execFileName')

        # 下载资源文件
        self.downlodFiles(remote_source_dir)

        if not self.break_update_xml:
            # 更新所有的数据
            is_upload = self.updateAllFiles()

            # 同步更新本地的配置文件
            if is_upload:
                self.update_local_xml_file()
                self.progressBar.setValue(100)
                self.printLog('成功', '更新完成.')
        else:
            self.printLog('错误', '连接异常,请稍后重试...')

        # 关闭界面
        # QCoreApplication.quit()
        # 重启程序
        QProcess().startDetached(os.path.join(os.getcwd(), execFileName))

    def downlodFiles(self, remote_source_dir):
        """
        下载资源文件
        Args:
            remote_source_dir (_type_): 远程资源文件目录
        """
        # 初始化参数
        file_num = 0

        # 进度条率
        total_file_num = len(self.add_files) + len(self.del_files) + len(self.replace_files)
        rate = float(100/total_file_num)

        # 创建新增资源文件的文件夹
        temp_add_files_dir = os.path.join(os.getcwd(), 'temp_add_files')  # 临时新增文件的路径
        for key, value in self.add_files.items():
            # 检查服务器是否中断
            is_break = self.checkBreakConnet()
            if is_break:
                self.break_update_xml = True
                return

            # 文件名
            file_name = Path(key).stem

            # 创建新增文件的临时文件夹
            std_file_dir = os.path.join(temp_add_files_dir, os.path.dirname(key))
            os.makedirs(std_file_dir, exist_ok=True)

            # 服务器上资源文件路径
            remote_file_url = os.path.join(remote_source_dir, key)
            remote_file_url = re.sub(r'\\', '/', remote_file_url)

            # 下载文件到本地
            # self.printLog('消息',f'新增资源文件 {file_name}.')
            try:
                local_temp_add_files_path = os.path.join(temp_add_files_dir, key)
                res = requests.get(remote_file_url, stream=True)
                with open(r''+local_temp_add_files_path, "wb") as pyFile:
                    for chunk in res.iter_content(chunk_size=1024):
                        if chunk:
                            pyFile.write(chunk)
                self.printLog('成功', f'下载新增资源文件 {local_temp_add_files_path} 成功.')

                # 本地资源文件路径
                add_file_to_path = os.path.join(os.getcwd(), key)
                self.temp_add_files_list.append([key, add_file_to_path])
            except:
                print(traceback.print_exc())
                self.printLog('错误', f'下载新增资源文件 {local_temp_add_files_path} 失败.')
                self.break_update_xml = True
                break

            # 文件的个数
            file_num += 1

            # 进度条
            self.progressBar.setValue(rate*file_num)

        # 创建替换资源文件的文件夹
        temp_replace_files_dir = os.path.join(os.getcwd(), 'temp_replace_files')  # 临时替换文件的路径
        for key, value in self.replace_files.items():
            # 检查服务器是否中断
            is_break = self.checkBreakConnet()
            if is_break:
                self.break_update_xml = True
                return

            # 文件名
            file_name = Path(key).stem

            # 创建新增文件的临时文件夹
            std_file_dir = os.path.join(temp_replace_files_dir, os.path.dirname(key))
            os.makedirs(std_file_dir, exist_ok=True)

            # 服务器上资源文件的路径
            remote_file_url = os.path.join(remote_source_dir, key)
            remote_file_url = re.sub(r'\\', '/', remote_file_url)

            # # 替换本地文件
            # self.printLog('消息',f'开始替换本地文件 {file_name}.')
            try:
                res = requests.get(remote_file_url, stream=True)
                local_temp_replace_files_path = os.path.join(temp_replace_files_dir, key)
                with open(r''+local_temp_replace_files_path, "wb") as pyFile:
                    for chunk in res.iter_content(chunk_size=1024):
                        if chunk:
                            pyFile.write(chunk)
                self.printLog('成功', f'下载替换本地文件 {local_temp_replace_files_path} 成功.')

                # 本地资源文件路径
                replace_file_to_path = os.path.join(os.getcwd(), key)
                self.temp_replace_files_list.append([key, replace_file_to_path])
            except:
                self.printLog('错误', f'下载替换本地文件 {local_temp_replace_files_path} 失败.')
                self.break_update_xml = True
                break

            # 进度条
            self.progressBar.setValue(rate*file_num)

            # 文件的个数
            file_num += 1

        # 删除资源文件
        for key, value in self.del_files.items():
            # 检查服务器是否中断
            is_break = self.checkBreakConnet()
            if is_break:
                self.break_update_xml = True
                return

            # 文件名
            file_name = Path(key).stem

            # 删除本地文件的路径
            del_local_file_path = os.path.join(os.getcwd(), key)

            self.printLog('消息', f'开始删除本地资源文件 {file_name}.')
            # 删除本地文件
            try:
                os.remove(del_local_file_path)
                self.printLog('成功', f'删除本地资源文件 {del_local_file_path} 成功.')
            except:
                self.printLog('错误', f'删除本地资源文件 {del_local_file_path} 失败.')
                self.break_update_xml = True
                break

            # 进度条
            self.progressBar.setValue(rate*file_num)

            # 文件的个数
            file_num += 1

    def update_local_xml_file(self):
        """
        更新本地的xml配置文件
        """
        # 本地 Updater.xml 的路径
        remote_remoteUrl = self.remote_xml_data.get('remoteProjectUrl')
        remote_xml_file_path = remote_remoteUrl+'/'+self.updater_config_file_name

        # 服务器 Updater.xml 的路径
        local_xml_file_path = os.path.join(os.getcwd(), self.updater_config_file_name)

        # 下载服务器上的 Updater.xml 文件
        self.printLog('消息', f'开始下载服务器上的 Updater.xml 文件.')
        try:
            res = requests.get(remote_xml_file_path, stream=True)
            remote_xml_file_path_str = res.content.decode('utf-8')
            if 'remoteProjectUrl' not in remote_xml_file_path_str:
                self.close()
            self.printLog('成功', f'下载服务器上的 Updater.xml 文件成功.')
        except:
            self.printLog('错误', f'下载服务器上的 Updater.xml 文件失败.')
            return

        self.printLog('消息', f'将服务器上的 Updater.xml 文件更新到本地.')
        try:
            with open(r''+local_xml_file_path, "wb") as pyFile:
                for chunk in res.iter_content(chunk_size=1024):
                    if chunk:
                        pyFile.write(chunk)
            self.printLog('成功', f'将服务器上的 Updater.xml 文件更新到本地成功.')
        except:
            self.printLog('错误', f'将服务器上的 Updater.xml 文件更新到本地失败.')

    def updateAllFiles(self):
        """
        更新所有文件
        """
        if self.temp_add_files_list:  # 临时增加文件的列表
            temp_add_files_dir = os.path.join(os.getcwd(), 'temp_add_files')  # 临时新增文件的路径
            for item in self.temp_add_files_list:
                try:
                    src_add_file, std_add_file_path = item[0], item[1]
                    src_add_file_path = os.path.join(temp_add_files_dir, src_add_file)
                    os.makedirs(os.path.dirname(std_add_file_path), exist_ok=True)
                    shutil.copy2(src_add_file_path, os.path.dirname(std_add_file_path))
                    self.printLog('成功', f'新增资源文件 {os.path.basename(std_add_file_path)} 成功.')
                except:
                    print(traceback.print_exc())
                    self.printLog('消息', f'新增资源文件 {os.path.basename(std_add_file_path)} 失败.')
                    return False
            return True

        if self.temp_replace_files_list:  # 临时替换文件的列表
            temp_replace_files_dir = os.path.join(os.getcwd(), 'temp_replace_files')  # 临时新增文件的路径
            for item in self.temp_replace_files_list:
                try:
                    src_replace_file, std_replace_file_path = item[0], item[1]
                    src_replace_file_path = os.path.join(temp_replace_files_dir, src_replace_file)
                    shutil.copy2(src_replace_file_path, std_replace_file_path)
                    self.printLog('成功', f'替换资源文件 {os.path.basename(std_replace_file_path)} 成功.')
                except:
                    print(traceback.print_exc())
                    self.printLog('消息', f'替换文件 {os.path.basename(std_replace_file_path)} 失败.')
                    return False
            return True

    def printLog(self, logType, msg, level=None):
        """
        消息日志打印
        Args:
            ui (object): 主界面对象
            level (int): 层级
            logType (string): 成功, 告警, 错误,消息
            msg (string): 打印的消息
        """
        # 当前时间
        curtime = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))

        # 输出行
        if logType == '消息':
            line = f'<span style="color:#ffffff;font-size:11px;">{msg}</span>'  # 白色字体
            ouput_line = f'<span style="font-size:11px;">【{curtime}】</span>' + line
        elif logType == '错误':
            line = f'<span style="color:#b73d2b;font-size:11px;">{msg}</span>'  # 红色字体
            ouput_line = f'<span style="font-size:11px;">【{curtime}】</span>' + line
        elif logType == '告警':
            line = f'<span style="color:#948f14;font-size:11px;">{msg}</span>'  # 黄色字体
            ouput_line = f'<span style="font-size:11px;">【{curtime}】</span>' + line
        elif logType == '成功':
            line = f'<span style="color:#237e26;font-size:11px;">{msg}</span>'  # 绿色字体
            ouput_line = f'<span style="font-size:11px;">【{curtime}】</span>' + line

        # 在指定的区域显示提示信息
        self.textBrowser.append(ouput_line)
        cursot = self.textBrowser.textCursor()
        cursot.movePosition(QTextCursor.EndOfLine)
        QApplication.processEvents()

    def checkBreakConnet(self):
        """
        检查是否已经断开连接
        """
        # 服务器上 Updater.xml 的路径
        remote_project_url = self.local_xml_data.get('remoteProjectUrl', '')
        remote_xml_url = remote_project_url+'/Updater.xml'

        # 下载服务器上的 Updater.xml
        res = requests.get(remote_xml_url, stream=True)
        remote_config_file_str = res.content.decode('utf-8')
        if 'remoteProjectUrl' in remote_config_file_str:
            return False
        else:
            return True

    def delTempFiles(self):
        """
        删除临时文件
        """
        current_project_path = os.getcwd()
        del_dir_list = ['temp_add_files', 'temp_replace_files']
        for std_path in del_dir_list:
            std_dir_path = os.path.join(current_project_path, std_path)
            if not os.path.isdir(std_dir_path):
                continue
            try:
                shutil.rmtree(std_dir_path)
            except:
                print(traceback.print_exc())
                self.printLog('错误', f'删除临时文件 {std_dir_path} 失败.')


if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = UpdateProcess()
    # window.center()
    window.show()
    sys.exit(app.exec_())
